// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Provide functionality to support Chrome Extension's extension
 * API.  Only functionality that is not directly accessible in Firefox is
 * implemented here.
 *
 * @author jasonstredwick@google.com (Jason Stredwick)
 */


goog.provide('mimic.Extension');

goog.require('bite.common.signal.Simple');
goog.require('console');
goog.require('mimic.utils');



/**
 * Creates an object to manage Chrome Extension's extension API.
 * @constructor
 */
mimic.Extension = function() {
  /**
   * Can be used to pass information when connections are requested.
   * @type {!bite.common.signal.Simple}
   */
  this.onConnect = new bite.common.signal.Simple();

  /**
   * Can be used to pass information when requests are sent.
   * @type {!bite.common.signal.Simple}
   */
  this.onRequest = new bite.common.signal.Simple();
};


/**
 * "Destroys" the Extension instance by "destroying" its data.
 */
mimic.Extension.prototype.destroy = function() {
  this.onConnect.clear();
  this.onRequest.clear();
};


/**
 * Converts a relative url to its full path with the Chrome Extension.
 * @param {string} rootUrl The root url of the Chrome Extension.
 * @param {string} relPath The relative path.
 * @return {string} The full path.
 */
mimic.Extension.getURL = function(rootUrl, relPath) {
  if (!relPath) {
    return rootUrl;
  }

  if (relPath[0] == '/') {
    relPath = relPath.substr(1);
  }

  return rootUrl + relPath;
};


/**
 * Sends a request to create a connection with the background capable of
 * passing messages back and forth.
 * @param {string} alias The alias of the owning extension.
 * @param {!Object} fromSandbox The sandbox object for code sending the
 *     request.
 * @param {!Object} toUnits An object containing a mapping of units by id; can
 *     only send to background scripts.  Message will be sent to all scripts.
 * @param {?number} id The id of the tab sending the message or null if a
 *     background script is sending the message.
 * @param {string=} opt_extensionId The extension id of the extension you want
 *     to connect to.  If omitted then the default is your own extension.
 *     The parameter is not specified as optional due to limitations of the
 *     javascript compiler, but it is indeed OPTIONAL.
 * @param {Object=} opt_connectInfo The information to about the connection.
 *     ConnectionInfo contains an optional name string.
 * @return {!Object} A Port object that allows for communication between the
 *     content and background scripts that were connected.
 */
mimic.Extension.prototype.connect =
    function(alias, fromSandbox, toUnits, id,
             opt_extensionId, opt_connectInfo) {
  var msg = '';

  // Process optional parameters and ensure that they have appropriate values.
  var idType = typeof(opt_extensionId);
  var connectionType = typeof(opt_connectInfo);

  // extensionId defaults to null and is explicitly checked as null below to
  // determine if the caller is a content script or the background (null).  An
  // empty string would be an invalid id given by a content script.
  var extensionId = idType == 'string' ? opt_extensionId : null;
  var connectInfo = connectionType == 'object' ? opt_connectInfo : null;

  if (opt_extensionId && idType != 'string') {
    if (idType == 'object') {
      connectInfo = opt_extensionId;
    } else {
      msg = 'Invalid inputs given to chrome.extension.connect.';
      fromSandbox['chrome']['extension']['lastError'] = msg;
      console.error(msg);
      return {'name': '', 'sender': {},
              'onDisconnect': mimic.utils.addFakeSignal(),
              'onMessage': mimic.utils.addFakeSignal(),
              'postMessage': goog.nullFunction};
    }
  }

  var name = (connectInfo && 'name' in connectInfo) ? connectInfo['name'] : '';

  // Prepare sender.
  var sender = {'id': alias};
  if (id != null) {
    // TODO (jasonstredwick): Fill out the rest of the tab structure.
    sender['tab'] = {'id': id};
  }

  // Prepare listeners
  var fromPortListeners = {
    'onDisconnect': new bite.common.signal.Simple(),
    'onMessage': new bite.common.signal.Simple()
  };

  var toPortListeners = {
    'onDisconnect': new bite.common.signal.Simple(),
    'onMessage': new bite.common.signal.Simple()
  };

  // Prepare 'from' port
  var fromPostMessage = (function() {
    var f = function(data) {
      toPortListeners['onMessage'].fire(data);
    };

    return function(data) { f(data); }
  })();

  // TODO (jasonstredwick): Consider making this a constructor rather than
  // building the object here.  Keep in mind that the format and naming is
  // a specification from the Chrome Extension API so the names must be
  // maintained and public during compilation.
  var fromPort = {
    'name': name,
    'sender': sender,
    'onDisconnect': mimic.utils.addSignal(fromPortListeners['onDisconnect']),
    'onMessage': mimic.utils.addSignal(fromPortListeners['onMessage']),
    'postMessage': fromPostMessage
  };

  // Prepare 'to' port.
  var toPostMessage = (function() {
    var f = function(data) {
      fromPortListeners['onMessage'].fire(data);
    };

    return function(data) { f(data); }
  })();

  var toPort = {
    'name': name,
    'sender': sender,
    'onDisconnect': mimic.utils.addSignal(toPortListeners['onDisconnect']),
    'onMessage': mimic.utils.addSignal(toPortListeners['onMessage']),
    'postMessage': toPostMessage
  };

  // Execute connect for each background script
  for (var key in toUnits) {
    // If a background script executed the connect then skip it.
    if (id == null && key == alias) {
      continue;
    }

    var toSandbox = toUnits[key].sandbox;
    var toSignals = toUnits[key].signals;

    // Prepare to send request.
    var send = (function() {
      var f = function() {
        toSandbox['chrome']['extension']['executeConnect'] = goog.nullFunction;
        toSignals.extension.onConnect.fire(toPort);
      };
      return function() { f(); };
    })();

    // Send request.
    toSandbox['chrome']['extension']['executeConnect'] = send;
    Components.utils.evalInSandbox('chrome.extension.executeConnect();',
                                   toSandbox);
  }

  return fromPort;
};


/**
 * Sends a request to the listeners in the background.
 * @param {string} alias The alias of the owning extension.
 * @param {!Object} fromSandbox The sandbox object for code sending the
 *     request.
 * @param {!Object} toUnits An object containing a mapping of units by id; can
 *     only send to background scripts.  Message will be sent to all scripts.
 * @param {?number} id The id of the tab sending the message or null if a
 *     background script is sending the message.
 * @param {string} extensionId The extension id of the extension you want
 *     to connect to.  If omitted then the default is your own extension.
 *     The parameter is not specified as optional due to limitations of the
 *     javascript compiler, but it is indeed OPTIONAL.
 * @param {Object} request The information to be sent out.  The request is
 *     expected to be a JSON-izable object.
 * @param {function(Object=)=} opt_response An optional callback to be called
 *     by the listeners who received the request.  The input to the callback
 *     is expected to be a JSON-izable object.
 */
mimic.Extension.prototype.sendRequest =
    function(alias, fromSandbox, toUnits, id,
             extensionId, request, opt_response) {
  var msg = '';
  // Process arguments by determining if optional parameters are present and
  // correct parameters as appropriate.
  try {
    // If extensionId is present then request and opt_response must be valid
    // types.
    if (typeof(extensionId) == 'string') {
      if (typeof(request) != 'object' ||
          (opt_response && typeof(opt_response) != 'function')) {
        throw '';
      }

    // If extensionId is not present then the following parameters should be
    // shifted down and extensionId is a the request and request is the
    // optional response.
    } else if (typeof(extensionId) != 'object' ||
               (request && typeof(request) != 'function')) {
      throw ''; // Shifted parameters are not present/correct types.
    } else {
      // Shift parameters so they are as expected and set defaults if optional
      // parameters are not present.
      opt_response = /** @type {function(Object=)} */ (request) || undefined;
      request = /** @type {Object} */ (extensionId);
      extensionId = '';
    }
  } catch (error) {
    msg = 'Invalid inputs given to chrome.extension.sendRequest.';
    fromSandbox['chrome']['extension']['lastError'] = msg;
    console.error(msg);
    opt_response && opt_response();
    return;
  }

  // Prepare a function that will be executed in the toSandbox yet execute the
  // the given response in the fromSandbox; i.e. the original caller.
  var response = undefined;
  if (opt_response && typeof(opt_response) == 'function') {
    response = (function() {
      var f = function(data) {
        // A function that will return the data while inside the sandbox.
        var getData = function() {
          return data;
        };

        // Wrapper function that calls the response function while inside the
        // sandbox.
        var executeResponse = function() {
          // Clean up sandbox in case the flow of execution adds the same
          // items.
          var getData = fromSandbox['chrome']['extension']['getData'];
          fromSandbox['chrome']['extension']['getData'] = goog.nullFunction;
          fromSandbox['chrome']['extension']['executeResponse'] =
              goog.nullFunction;

          opt_response(getData());
        };

        fromSandbox['chrome']['extension']['getData'] = getData;
        fromSandbox['chrome']['extension']['executeResponse'] =
            executeResponse;

        Components.utils.evalInSandbox('chrome.extension.executeResponse();',
                                       fromSandbox);
      };

      return function(data) { f(data); };
    })();
  } else if (opt_response) {
    msg = 'Invalid response given to chrome.extension.sendRequest.';
    fromSandbox['chrome']['extension']['lastError'] = msg;
    console.error(msg);
    opt_response && opt_response(); // Already executing in appropriate sandbox
    return;
  }

  // Prepare sender
  var sender = {'id': alias};
  if (id != null) {
    // TODO (jasonstredwick): Fill out the rest of the tab structure.
    sender['tab'] = {'id': id};
  }

  // Send message to each background script
  for (var key in toUnits) {
    // If a background script sent the message then skip it.
    if (id == null && key == alias) {
      continue;
    }

    var toSandbox = toUnits[key].sandbox;
    var toSignals = toUnits[key].signals;

    // Prepare to send request.
    var send = (function() {
      var f = function() {
        toSandbox['chrome']['extension']['executeSend'] = goog.nullFunction;
        toSignals.extension.onRequest.fire(request, sender, response);
      };
      return function() { f(); };
    })();

    toSandbox['chrome']['extension']['executeSend'] = send;

    // Send request.
    Components.utils.evalInSandbox('chrome.extension.executeSend();',
                                   toSandbox);
  }
};

